//! Provides a RESTful web server managing some Profiles.
//!
//! API will be:
//!
//! - `GET /profiles`: return a JSON list of Profiles.
//! - `POST /profiles`: create a new Profile.
//! - `PUT /profiles/:id`: update a specific Profile.
//! - `DELETE /profiles/:id`: delete a specific Profile.
//!

use axum::{
    body::{boxed, Body},
    error_handling::HandleErrorLayer,
    extract::{Path, Query, State},
    http::{Response, StatusCode},
    response::IntoResponse,
    routing::{get, patch},
    Json, Router,
};
use net2::{TcpBuilder, unix::UnixTcpBuilderExt};
use serde::{Deserialize, Serialize};
use std::{
    collections::HashMap,
    sync::{Arc, RwLock},
    time::Duration, net::{SocketAddrV4, Ipv4Addr, TcpListener},
};
use std::{
    net::{IpAddr, Ipv6Addr, SocketAddr},
    path::PathBuf,
    str::FromStr,
};
use tokio::{fs, sync::mpsc::UnboundedSender};
use tower::{BoxError, ServiceBuilder, ServiceExt};
use tower_http::services::ServeDir;
use tower_http::trace::TraceLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use uuid::Uuid;

use shared::*;

use crate::{netem_cmd::*, netem_server::CtrlMsg, Opt, OPTS};

type ServerState = Arc<RwLock<ServerStateInner>>;

struct ServerStateInner {
    db: HashMap<Uuid, NetemProfile>,
    tx: UnboundedSender<CtrlMsg>,
}

fn init_tracing() {
    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "voip_netem=debug,tower_http=debug".into()),
        )
        .with(tracing_subscriber::fmt::layer())
        .init();
}

pub async fn run_server_loop(opt: Opt, tx: UnboundedSender<CtrlMsg>) {
    init_tracing();

    let default_profile = NetemProfile::default();
    let mut db = HashMap::new();
    db.insert(default_profile.id.clone(), default_profile);
    let state = Arc::new(RwLock::new(ServerStateInner { tx, db }));

    // Compose the routes
    let app = Router::new()
        .route("/api/hello", get(hello))
        .route("/api/server_info", get(server_info))
        .route("/api/profiles", get(profiles_index).post(profiles_create))
        .route(
            "/api/profiles/:id",
            patch(profiles_update)
                .delete(profiles_delete)
                .get(profiles_get),
        )
        .fallback_service(get(|req| async move {
            match ServeDir::new(&opt.static_dir).oneshot(req).await {
                Ok(res) => {
                    let status = res.status();
                    match status {
                        StatusCode::NOT_FOUND => {
                            let index_path = PathBuf::from(&opt.static_dir).join("index.html");
                            let index_content = match fs::read_to_string(index_path).await {
                                Err(_) => {
                                    return Response::builder()
                                        .status(StatusCode::NOT_FOUND)
                                        .body(boxed(Body::from("index file not found")))
                                        .unwrap()
                                }
                                Ok(index_content) => index_content,
                            };

                            Response::builder()
                                .status(StatusCode::OK)
                                .body(boxed(Body::from(index_content)))
                                .unwrap()
                        }
                        _ => res.map(boxed),
                    }
                }
                Err(err) => Response::builder()
                    .status(StatusCode::INTERNAL_SERVER_ERROR)
                    .body(boxed(Body::from(format!("error: {err}"))))
                    .expect("error response"),
            }
        }))
        .layer(
            ServiceBuilder::new()
                .layer(HandleErrorLayer::new(|error: BoxError| async move {
                    if error.is::<tower::timeout::error::Elapsed>() {
                        Ok(StatusCode::REQUEST_TIMEOUT)
                    } else {
                        Err((
                            StatusCode::INTERNAL_SERVER_ERROR,
                            format!("Unhandled internal error: {}", error),
                        ))
                    }
                }))
                .timeout(Duration::from_secs(10))
                .layer(TraceLayer::new_for_http())
                .into_inner(),
        )
        .layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()))
        .with_state(state);

    let listener: TcpListener = TcpBuilder::new_v4()
        .unwrap()
        .reuse_address(true)
        .unwrap()
        .reuse_port(true)
        .unwrap()
        .bind(format!("{}:{}", opt.addr, opt.web_port))
        .unwrap()
        .listen(32)
        .unwrap();
    axum::Server::from_tcp(listener)
        .unwrap()
        .serve(app.into_make_service())
        .await
        .map_err(|err| {
            println!("exit error {}", err);
        });
}

// The query parameters for profiles index
#[derive(Debug, Deserialize, Default)]
pub struct Pagination {
    pub offset: Option<usize>,
    pub limit: Option<usize>,
}

async fn profiles_index(
    pagination: Option<Query<Pagination>>,
    State(state): State<ServerState>,
) -> impl IntoResponse {
    let state = state.read().unwrap();

    let Query(pagination) = pagination.unwrap_or_default();

    let profiles = state
        .db
        .values()
        .skip(pagination.offset.unwrap_or(0))
        .take(pagination.limit.unwrap_or(usize::MAX))
        .cloned()
        .collect::<Vec<_>>();

    Json(profiles)
}

async fn profiles_create(
    State(state): State<ServerState>,
    Json(input): Json<NetemProfile>,
) -> impl IntoResponse {
    let profile = NetemProfile {
        id: Uuid::new_v4(),
        ..input
    };

    state
        .write()
        .unwrap()
        .db
        .insert(profile.id, profile.clone());

    (StatusCode::CREATED, Json(profile))
}

async fn profiles_update(
    Path(id): Path<Uuid>,
    State(state): State<ServerState>,
    Json(new_profile): Json<NetemProfile>,
) -> Result<impl IntoResponse, StatusCode> {
    let mut state = state.write().unwrap();

    let mut old_profile = state.db.get(&id).cloned().ok_or(StatusCode::NOT_FOUND)?;

    if new_profile.active != old_profile.active {
        if new_profile.active {
            let _ = state.tx.send(CtrlMsg::Active(new_profile.cfgs.clone()));
        } else {
            println!("Deactive");
            let _ = state.tx.send(CtrlMsg::Deactive);
        }
    }

    state.db.insert(id, new_profile.clone());

    Ok(Json(new_profile))
}

async fn profiles_delete(
    Path(id): Path<Uuid>,
    State(state): State<ServerState>,
) -> impl IntoResponse {
    if state.write().unwrap().db.remove(&id).is_some() {
        StatusCode::NO_CONTENT
    } else {
        StatusCode::NOT_FOUND
    }
}

async fn profiles_get(
    Path(id): Path<Uuid>,
    State(state): State<ServerState>,
) -> Result<impl IntoResponse, StatusCode> {
    tracing::trace!("fetch {}", id);
    let mut profile = state
        .read()
        .unwrap()
        .db
        .get(&id)
        .cloned()
        .ok_or(StatusCode::NOT_FOUND)?;
    tracing::trace!("got {:?}", profile);
    Ok(Json(profile))
}

async fn hello() -> impl IntoResponse {
    "hello from server!"
}

async fn server_info() -> Result<impl IntoResponse, StatusCode> {
    let mut server_info = ServerInfo {
        name: OPTS.server_name.clone(),
        tcp_ports: OPTS.server_tcp_port.clone(),
        udp_ports: OPTS.server_udp_port.clone(),
    };
    tracing::trace!("got {:?}", server_info);
    Ok(Json(server_info))
}